<?php
// +----------------------------------------------------------------------
// | 微信支付
// +----------------------------------------------------------------------

namespace helper\tencent;

use helper\facade\IPayment;

class Pay implements IPayment
{
    private array $config;

    private string $host = 'https://api.mch.weixin.qq.com';

    public function __construct($config)
    {
        $this->config = [
            'appid'       => $config['appid'],// 应用唯一标识
            'secret'      => $config['secret'],// 应用密钥 AppSecret
            'mchid'       => $config['mchid'],// 微信支付分配的商户号
            'serial_no'   => $config['serial_no'] ?? '',// 商户API证书序列号
            'notify_url'  => $config['notify_url'],// 接收微信支付异步通知回调地址
            'aes_key'     => $config['aes_key'],
            'private_key' => $config['private_key'],// apiclient_key.pem
            'public_key'  => $config['public_key'] // wechatpay_***.pem
        ];
    }

    /**
     * app支付
     * 1、预支付交易单（服务端）1)统一下单 2)调起支付接口
     * 2、调起支付（客户端）
     * 3、支付结果通知（服务端）
     * @param string $subject 商品描述
     * @param string $out_trade_no 商户订单号
     * @param float $total_amount 商品金额
     * @return string
     */
    public function app(string $subject, string $out_trade_no, float $total_amount)
    {
        // 1)APP下单API
        $array = [
            "description"  => $subject,//商品描述
            "out_trade_no" => $out_trade_no,//商户订单号
            "total_amount" => $total_amount
        ];
        $prepay_id = $this->pay($array, 'app');

        // 2)APP调起支付API
        $data["appid"]     = $this->config['appid'];//应用ID
        $data["partnerid"] = $this->config['mchid'];//商户号
        $data["prepayid"]  = $prepay_id;//预支付交易会话ID
        $data["package"]   = "Sign=WXPay";//扩展字段
        $data["noncestr"]  = $this->getRandChar();//随机字符串
        $data["timestamp"] = time();//时间戳
        $data["sign"]      = $this->getSign($data);//签名
        return json_encode($data);
    }

    /**
     * Native下单API
     * @return void
     */
    public function page(string $subject, string $out_trade_no, float $total_amount, string $return_url = '', $time = 600)
    {
        return $this->pay([
            'subject' => $subject,
            'out_trade_no' => $out_trade_no,
            'total_amount' => $total_amount,
            'time_expire' => $time
        ], 'native');
    }

    /**
     * 下单
     * @param array $array
     * @param string $method
     * @return mixed|string
     */
    public function pay(array $array, string $method = 'jsapi')
    {
        $url    = $this->host . '/v3/pay/transactions/' . $method;
        $method = 'POST';
        $body   = [
            "appid"        => $this->config['appid'],
            "mchid"        => $this->config['mchid'],
            "description"  => $array['subject'],//商品描述
            "out_trade_no" => $array['out_trade_no'],//商户订单号
            "time_expire"  => isset($array['time_expire']) ? date('c', time() + $array['time_expire']) : '',//交易结束时间
            "attach"       => $array['attach'] ?? '',//附加数据
            "notify_url"   => $this->config['notify_url'],//通知地址
            //"goods_tag"   => '',//订单优惠标记
            "amount"       => [
                "total"    => $array['total_amount'] * 100,// 订单总金额，单位为分。
                "currency" => "CNY"
            ]
        ];

        if (isset($array['openid'])) {
            $body['payer']['openid'] = $array['openid'];
        }

        $response = $this->request($method, $url,
            [
                'json'    => $body,
                'headers' => [
                    'Accept: application/json',
                    'Content-Type: application/json',
                    'User-Agent:' . $_SERVER['HTTP_USER_AGENT'],
                    'Authorization:' . $this->auth($url, $method, json_encode($body))
                ]
            ]
        );
        if ($response['statusCode'] != 200) {
            return '';
        }

        // 2)APP调起支付API
        $data["appid"]     = $this->config['appid'];//应用ID
        $data["partnerid"] = $this->config['mchid'];//商户号
        $data["prepayid"]  = $response['body']['prepay_id'];//预支付交易会话ID
        $data["package"]   = "Sign=WXPay";//扩展字段
        $data["noncestr"]  = $this->getRandChar();//随机字符串
        $data["timestamp"] = time();//时间戳
        $data["sign"]      = $this->getSign($data);//签名
        return json_encode($data);
    }

    /**
     * 支付产品 - APP支付
     * 查询订单
     * @param string $out_trade_no 商户订单号
     * @return array
     */
    public function orderQuery(string $out_trade_no)
    {
        $url = $this->host . "/v3/pay/transactions/out-trade-no/$out_trade_no?mchid={$this->config['mchid']}";
        return $this->request('GET', $url, []);
    }

    /**
     * 支付产品 - APP支付
     * 关闭订单
     * @param string $out_trade_no 商户订单号
     * @return array
     */
    public function closeOrder(string $out_trade_no)
    {
        $url           = $this->host . "/v3/pay/transactions/out-trade-no/$out_trade_no/close";
        $data["mchid"] = $this->config['mchid'];
        return $this->request('POST', $url, [], $data);
    }

    /**
     * 支付产品 - APP支付
     * 申请退款
     * @param string $out_trade_no string 1.out_trade_no商户订单号 2.transaction_id 微信订单号 二选一
     * @param $refund_amount
     * @param $total_amount
     * @return bool
     */
    public function refund(string $out_trade_no, $refund_amount, $total_amount, $reason = '退款'): bool
    {
        $url    = $this->host . "/v3/refund/domestic/refunds";
        $method = 'POST';
        $body   = [
            "out_trade_no"  => $out_trade_no,// 商户订单号
            "out_refund_no" => $out_trade_no,// 商户退款单号
            "reason"        => $reason,//退款原因
            "notify_url"    => $this->config['notify_url'],// 退款结果回调url
            "amount"        => [
                "refund"   => $refund_amount * 100,// 退款金额
                "total"    => $total_amount * 100,// 原订单金额，单位为分。
                "currency" => "CNY"
            ]
        ];

        $response = $this->request($method, $url,
            [
                'json'    => $body,
                'headers' => [
                    'Accept: application/json',
                    'Content-Type: application/json',
                    'User-Agent:' . $_SERVER['HTTP_USER_AGENT'],
                    'Authorization:' . $this->auth($url, $method, json_encode($body))
                ]
            ]
        );
        if ($response['statusCode'] != 200) {
            return false;
        }
        return true;
    }

    /**
     * 支付产品 - APP支付
     * 查询退款
     * @param string $out_trade_no out_trade_no
     * @return array
     */
    public function refund_query(string $out_trade_no): array
    {
        $url = $this->host . "/v3/refund/domestic/refunds/" . $out_trade_no;
        $method = 'GET';
        $response = $this->request($method, $url);
        if ($response['statusCode'] != 200) {
            return [];
        }
        return $response;
    }

    /**
     * 支付工具 - 三:企业付款 https://pay.weixin.qq.com/wiki/doc/api/tools/mch_pay.php?chapter=14_1
     * 企业付款到零钱 [证书]
     * @param string $out_trade_no 订单号
     * @param float $amount 金额
     * @param string $account 账号
     * @param string $name 姓名
     * @param string $remark 备注
     * @return array
     */
    public function transfer(string $out_trade_no, float $amount, string $account, string $name, string $remark): array
    {
        $url               = "https://api.mch.weixin.qq.com/mmpaymkttransfers/promotion/transfers";
        $data['mch_appid'] = $this->config['appid'];//商户账号appid
        $data['mchid']     = $this->config['mchid'];//商户号
        //$data['device_info'] = '';//设备号
        $data['nonce_str']        = $this->getRandChar(32);//随机字符串
        $data["partner_trade_no"] = $out_trade_no;//商户订单号
        $data['openid']           = $account;//用户openid
        $data['check_name']       = 'FORCE_CHECK';//校验用户姓名选项 NO_CHECK：不校验真实姓名,FORCE_CHECK：强校验真实姓名
        $data['re_user_name']     = $name;//收款用户姓名
        $data['amount']           = $amount;//金额
        $data['desc']             = $remark;//企业付款备注
        $data['spbill_create_ip'] = $this->get_client_ip();//Ip地址
        $data["sign"]             = $this->getSign($data);
        $response = $this->request('POST', $url,
            [
                'json'    => $data,
                'headers' => [
                    'Accept: application/json',
                    'Content-Type: application/json',
                ]
            ]
        );
        if ($response['statusCode'] != 200) {
            return '';
        }
        return $response['body']['prepay_id'];
    }

    /**
     * 支付工具 - 三:企业付款
     * 查询企业付款 [证书]
     * @param $trade_no string
     * @return array
     */
    public function transfer_query($trade_no)
    {
        $url  = "https://api.mch.weixin.qq.com/mmpaymkttransfers/gettransferinfo";
        $data = [
            'nonce_str'        => $this->getRandChar(32),//随机字符串
            'partner_trade_no' => $trade_no,//商户订单号
            'mch_id'           => $this->config['mchid'],//商户号
            'appid'            => $this->config['appid']//Appid
        ];
        $data["sign"] = $this->getSign($data);
        return [];
    }

    /**
     * 支付工具 - 四:分账
     * 查询企业付款 [证书]
     */
    public function profitSharing()
    {

    }

    /**
     * 企业付款到个人银行卡
     * https://pay.weixin.qq.com/wiki/doc/api/tools/mch_pay_yhk.php?chapter=25_2
     * @return void
     */
    public function pay_bank()
    {

    }

    /**
     * 签名生成
     * @return string
     */
    public function auth($url, $http_method = 'GET', $body = '')
    {
        // Authorization: <schema> <token>
        $url_parts       = parse_url($url);
        $timestamp       = time();
        $nonce_str       = $this->getRandChar();
        $mch_private_key = file_get_contents($this->config['private_key']);

        $canonical_url = ($url_parts['path'] . (!empty($url_parts['query']) ? "?${url_parts['query']}" : ""));
        $message       = $http_method . "\n" .
            $canonical_url . "\n" .
            $timestamp . "\n" .
            $nonce_str . "\n" .
            $body . "\n";
        openssl_sign($message, $raw_sign, $mch_private_key, 'sha256WithRSAEncryption');
        $sign = base64_encode($raw_sign);

        $schema = 'WECHATPAY2-SHA256-RSA2048';
        $token  = sprintf('mchid="%s",nonce_str="%s",timestamp="%d",serial_no="%s",signature="%s"',
            $this->config['mchid'], $nonce_str, $timestamp, $this->config['serial_no'], $sign);
        return $schema . ' ' . $token;
    }

    /**
     * 平台证书
     */
    public function certificates()
    {
        $url = 'https://api.mch.weixin.qq.com/v3/certificates';
        $method = 'GET';
        $response = $this->request($method, $url,
            [
                'json' => [],
                'headers' => [
                    'Accept: application/json',
                    'Content-Type: application/json',
                    'User-Agent:'. $_SERVER['HTTP_USER_AGENT'],
                    'Authorization:'. $this->auth($url, $method)
                ]
            ]
        );
        if ($response['statusCode'] == 200) {
            return $response['body'];
        }
        return [];
    }

    /**
     * 异步回调验证
     * @param array $post
     * @return array
     */
    public function verifyNotify(array $post): array
    {
        // 检查平台证书序列号
        if ($post['header']['wechatpay-serial'] == $this->config['serial_no']) {
            return [];
        }

        // 构造验签名串 应答时间戳,应答随机串,应答主体
        $data      = $post['header']['wechatpay-timestamp'] . "\n" . $post['header']['wechatpay-nonce'] . "\n"
            . $post['body'] . "\n";
        $signature = base64_decode($post['header']['wechatpay-signature']);// 解密应答签名
        // 微信支付平台证书,通过java工具下载,并非apiclient_cert.pem
        $pub_key_id = openssl_pkey_get_public(file_get_contents($this->config['public_key']));
        $ok         = openssl_verify($data, $signature, $pub_key_id, OPENSSL_ALGO_SHA256);
        if ($ok != 1) {
            return [];
        }
        return $post;
    }

    /**
     * 签名计算
     * @param array $data
     * @return string
     */
    public function getSign($data)
    {
        $mch_private_key = file_get_contents($this->config['private_key']);
        $message         = $this->config['appid'] . "\n" .
            $data['timestamp'] . "\n" .
            $data['noncestr'] . "\n" .
            $data['prepayid'] . "\n";
        openssl_sign($message, $raw_sign, $mch_private_key, 'sha256WithRSAEncryption');
        return base64_encode($raw_sign);
    }

    /**
     * 作用：产生随机字符串，不长于32位
     * @param $length int
     * @return string
     */
    function getRandChar($length = 32)
    {
        $str    = null;
        $strPol = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789abcdefghijklmnopqrstuvwxyz";
        $max    = strlen($strPol) - 1;
        for ($i = 0; $i < $length; $i++) {
            $str .= $strPol[rand(0, $max)];//rand($min,$max)生成介于min和max两个数之间的一个随机整数
        }
        return $str;
    }

    /**
     * 发起请求 string $method, $uri = '', array $options = []
     * @param string $method
     * @param string $url
     * @param array $options
     * @return array
     */
    private function request(string $method, string $url, array $options = [])
    {
        $curl = curl_init();
        curl_setopt($curl, CURLOPT_CUSTOMREQUEST, $method);
        curl_setopt($curl, CURLOPT_URL, $url);
        if (isset($options['headers'])) {
            curl_setopt($curl, CURLOPT_HTTPHEADER, $options['headers']);
        }
        curl_setopt($curl, CURLOPT_FAILONERROR, false);
        curl_setopt($curl, CURLOPT_RETURNTRANSFER, true);
        curl_setopt($curl, CURLOPT_HEADER, true);
        if (1 == strpos("$" . $url, "https://")) {
            curl_setopt($curl, CURLOPT_SSL_VERIFYPEER, false);
            curl_setopt($curl, CURLOPT_SSL_VERIFYHOST, false);
        }
        if (isset($options['json'])) {
            curl_setopt($curl, CURLOPT_POSTFIELDS, json_encode($options['json']));
        }

        $result      = curl_exec($curl);
        $header_size = curl_getinfo($curl, CURLINFO_HEADER_SIZE);
        $rheader     = substr($result, 0, $header_size);
        $rbody       = substr($result, $header_size);
        $httpCode    = curl_getinfo($curl, CURLINFO_HTTP_CODE);
        return [
            'statusCode' => $httpCode,
            'body'       => json_decode($rbody, true)
        ];
    }

    /**
     * 解密
     *
     * @param string $associatedData AES GCM additional authentication data
     * @param string $nonceStr AES GCM nonce
     * @param string $ciphertext AES GCM cipher text
     *
     * @return string|bool      Decrypted string on success or FALSE on failure
     */
    public function decryptToString($associatedData, $nonceStr, $ciphertext)
    {
        $ciphertext = base64_decode($ciphertext);
        if (strlen($ciphertext) <= 16) {
            return false;
        }
        // openssl (PHP >= 7.1 support AEAD)
        $ctext   = substr($ciphertext, 0, -16);
        $authTag = substr($ciphertext, -16);
        return openssl_decrypt($ctext, 'aes-256-gcm', $this->config['aes_key'], OPENSSL_RAW_DATA, $nonceStr,
            $authTag, $associatedData);
    }

    /**
     * 将一个数组转换为 XML 结构的字符串
     * @param array $arr 要转换的数组
     * @param int $level 节点层级, 1 为 Root.
     * @return string XML 结构的字符串
     */
    function array2xml($arr, $level = 1) {
        $s = $level == 1 ? "<xml>" : '';
        foreach($arr as $tagname => $value) {
            if (is_numeric($tagname)) {
                $tagname = $value['TagName'];
                unset($value['TagName']);
            }
            if(!is_array($value)) {
                $s .= "<{$tagname}>".(!is_numeric($value) ? '<![CDATA[' : '').$value.(!is_numeric($value) ? ']]>' : '')."</{$tagname}>";
            } else {
                $s .= "<{$tagname}>" . $this->array2xml($value, $level + 1)."</{$tagname}>";
            }
        }
        $s = preg_replace("/([\x01-\x08\x0b-\x0c\x0e-\x1f])+/", ' ', $s);
        return $level == 1 ? $s."</xml>" : $s;
    }

    function http_post($url, $param, $wxchat) {
        $oCurl = curl_init();
        if (stripos($url, "https://") !== FALSE) {
            curl_setopt($oCurl, CURLOPT_SSL_VERIFYPEER, FALSE);
            curl_setopt($oCurl, CURLOPT_SSL_VERIFYHOST, FALSE);
        }
        if (is_string($param)) {
            $strPOST = $param;
        } else {
            $aPOST = array();
            foreach ($param as $key => $val) {
                $aPOST[] = $key . "=" . urlencode($val);
            }
            $strPOST = join("&", $aPOST);
        }
        curl_setopt($oCurl, CURLOPT_URL, $url);
        curl_setopt($oCurl, CURLOPT_RETURNTRANSFER, 1);
        curl_setopt($oCurl, CURLOPT_POST, true);
        curl_setopt($oCurl, CURLOPT_POSTFIELDS, $strPOST);
        if($wxchat){
            curl_setopt($oCurl,CURLOPT_SSLCERT,$wxchat['api_cert']);
            curl_setopt($oCurl,CURLOPT_SSLKEY,$wxchat['api_key']);
            curl_setopt($oCurl,CURLOPT_CAINFO,$wxchat['api_ca']);
        }
        $sContent = curl_exec($oCurl);
        $aStatus = curl_getinfo($oCurl);
        curl_close($oCurl);

        if (intval($aStatus["http_code"]) == 200) {
            return $sContent;
        } else {
            return false;
        }
    }

    /**
     *   获取当前服务器的IP
     */
    public function get_client_ip()
    {
        if ($_SERVER['REMOTE_ADDR']) {
            $cip = $_SERVER['REMOTE_ADDR'];
        } elseif (getenv("REMOTE_ADDR")) {
            $cip = getenv("REMOTE_ADDR");
        } elseif (getenv("HTTP_CLIENT_IP")) {
            $cip = getenv("HTTP_CLIENT_IP");
        } else {
            $cip = "unknown";
        }
        return $cip;
    }
}
